Skip to content

wave 2: eight backlog lanes as one integration train (15 items close) - #311

Merged
wshallwshall merged 38 commits into
mainfrom
w2-integration
Aug 10, 2026
Merged

wave 2: eight backlog lanes as one integration train (15 items close)#311
wshallwshall merged 38 commits into
mainfrom
w2-integration

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Wave 2 of the backlog-clearing plan, as a single integration branch rather than eight lane PRs.

BACKLOG #333 #344 #1012 #1033 #1035 #1036 #1052 #1053 #1076 #1079 #1089 #1090 #1092 #1095 #1103

Why one PR and not eight

backlog-hygiene requires docs/BACKLOG.md in any diff that carries a BACKLOG #N token and touches
messagefoundry/, ide/ or messagefoundry_webconsole/. Five lanes touch engine or script code and
none may edit the ledger -- a single sole-writer lane owns it for the wave, which is what stops
concurrent banner edits colliding. As independent PRs those five could not satisfy the required
context. The train carries the code and the banner commit together, satisfying it once.

Contents

Eight lanes, each verified contained by merge-base --is-ancestor, plus a banner commit.
38 commits, 68 files.

Two items stay OPEN deliberately -- please do not tidy them

  • #1040 -- claim_check.py still interpolates a session-supplied claim note into a stderr block
    that ends in a runnable release command.
  • #1210 -- arms 1 and 4 shipped; arm 2 is unbuilt and arm 3 was rejected on measurement, so the
    FD/RSS sum still cannot distinguish real growth from a misresolution.

#64 also remains open, as previously instructed.

Verification

  • Eight lanes merged clean; the instrument was checked before its result was trusted --
    merge-tree --write-tree against a known-conflicting branch returns exit 1, so exit 0 here means
    clean rather than blind.
  • Merges cleanly onto main, re-confirmed after a fresh fetch.
  • Ledger re-derived with parse_items from the train rather than carried forward: live 241, open 209,
    closed-in-live 32, archive 236 -- namespace 477 conserved. No duplicate numbers, no item declaring
    more than one status.
  • Full suite on the merged tree: 11,494 passed, 855 skipped, 1 failed -- the single failure was
    test_gate_installed_parity, which was pending a gate reinstall. That reinstall has since been
    performed and the installed gate is now in sync with main, so the condition no longer holds.
  • Diff scanned for customer, site and partner tokens and for private paths before pushing: zero hits,
    with the scan's negative control confirmed firing on synthetic bad input.

Note on merge method

This should land as a merge commit, not a squash. Each commit subject names the item its ledger
banner cites; a squash would collapse 38 commits into one and destroy that attribution. The repository
default for auto-merge is squash, so this needs to be explicit.

…warning is anonymous (#333)

Steps 1 and 2 of #333, and step 1 is a hard precondition: every posture surface
built on this detector would inherit its false negative and report the worst
real case as clean.

_ODBC_TLS_HINT_RE matched against odbc_params KEYS ONLY, so
odbc_params={"SSLmode": "disable"} -- psqlODBC's explicit *no TLS* spelling --
read as "the operator has taken TLS ownership" and dropped the construction
reminder from WARNING to DEBUG. Same for sslmode=allow/prefer, MySQL
DISABLED/PREFERRED, and Encrypt=no/0/false/off.

Adds a value deny-list beside the key regex, and one shared classifier
(generic_odbc_no_tls_params / generic_odbc_tls_unenforced) so the posture
readers landing next cannot diverge from what the log says. A deny-list rather
than an allow-list because an arbitrary driver's VERIFYING spellings are
unbounded; the known bad values are few and vendor-documented. The
encrypted-but-unverified class (sslmode=require) is deliberately NOT on the
list and the reason is written down at the constant.

The line also named no connection, so a site running several generic DB
connections got a warning it could not act on. Both directions now name
themselves; Source gains an optional `name` filled by the runner's
_source_config, since it carried none.

Tests: the pinned "any TLS-ish key silences it" assertion is inverted into a
pair (verify-full stays quiet, nine no-TLS spellings warn), plus a naming test
per direction. Red first: the nine value cases failed with an EMPTY warning
list before the fix.
`_PATH_RE` matches `\d+` for the field, component and subcomponent, so `PID-5.0`
parsed with `comp=0`. Every consumer then indexes `x[n - 1]`, which for `0` is
Python's `x[-1]` -- the LAST part. Measured against the pre-guard tree:

  Peek.field("PID-5.0")       -> "DOE"   (a component nobody asked for)
  Message.field("PID-5.0")    -> "L"     (a DIFFERENT wrong answer from the
                                          same path -- the two read surfaces
                                          silently disagreed)
  Message.set("PID-5.0", v)   -> overwrote the LAST component
  Message.set("PID-5.1.0", v) -> overwrote the last subcomponent
  Message.set("PID-0", "XXX") -> rewrote the SEGMENT ID in the encoded message

None of those raised, so on a first deployment a message would deliver looking
successful with a value in the wrong place -- no exception, no ERROR
disposition, no dead-letter.

The guard goes at `parse_path`, the one path-parsing chokepoint for both the
read and the write side, and is the one `parsing/x12/message.py::_parse_path`
has always had. `HL7PeekError` is a `ValueError`, so a bad path from a
Router/Handler is an ordinary message failure on the ERROR/dead-letter path
rather than a crashed connection, and `store/content_search.py` (behind the
API's `field_path` query parameter) already maps it to a 4xx.

Field 0 is rejected too, not just the filed component case: it is python-hl7's
segment-id slot, never a valid HL7 address, and the write there is the worst of
the five.

60 tests, confirmed red first: 53 of 60 fail against the pre-guard code. The 7
that pass are the positive controls (ordinary 1-based paths still read and
write), which is what keeps the guard from being over-broad. Each write arm
asserts the message is BYTE-IDENTICAL afterwards -- a guard that raised after
mutating would pass a raises-check alone.
…nt (BACKLOG #1012)

The gate summary line enumerated FIVE verdict states and stated a total that counted six
states' worth of cells -- 344 components against a stated 345 -- because the enumeration was
retyped by hand in the format string and `needs-review` was never added to it. Nothing
compared the two numbers, so the line could not be reconciled against itself. It is the line
people quote, and it was quoted wrong for a full session.

Three changes, of which only the first is the reported defect:

* VERDICT_ORDER is DERIVED from the `Verdict` Literal via get_args, so the enumeration and the
  type cannot drift. VERDICTS is now derived from it rather than retyped a third time.
* verdict_breakdown() is the one place a distribution is assembled. Its components come from a
  Counter over the verdicts cells CARRY and its total from len(cells) -- two independent
  readings of one population -- and it refuses rather than printing when they disagree. Not an
  `assert` (which -O deletes). Both the gate summary line and `--status` call it, so the two
  renderings of one population can no longer disagree, which is how the omission survived.
* render_current's six hand-written table rows above a hand-written Total now walk the same
  enumeration; a state with no row REFUSES instead of rendering nowhere.

The rendered page is byte-identical: verified by rendering a 16-cell population covering every
state through the pre-change and post-change modules and comparing (2,024 bytes both sides,
with a negative control confirming the comparison can tell two renders apart).

Every new test was made to go RED on purpose first:
  * restore the old five-state f-string  -> the summary test fails with
    "scanned 3 cells (1 pass / 0 partial / 0 fail / 0 na / 1 unverified)", the defect in
    miniature: components sum to 2 against a stated 3
  * delete the needs-review row from _VERDICT_ROW -> render_current raises ScorecardError
  * retype VERDICT_ORDER by hand without needs-review -> two arms fail

One arm survives that third mutation by construction and its docstring now says so: the
agreement test builds its population from VERDICT_ORDER, so it cannot police that tuple.
…090)

`write_reference_snapshot` called `json.dumps(v)` over a `Mapping[str, Any]`
with no `default=`, on all three backends. `tomllib` materializes a TOML date as
`datetime.date`, which `json.dumps` cannot encode. Measured against the pre-fix
tree with an ordinary reference TOML carrying `effective = 2026-01-01`:

  _load_file_source(...)                   -> {'acme': {'effective': date(...)}}
  store.write_reference_snapshot(rows=...) -> TypeError: Object of type date is
                                              not JSON serializable

Both the flat and the nested-table TOML shapes failed, and the sync's generic
handler then keeps the last-good snapshot and logs one WARNING naming only the
exception class -- so on a first deployment every Handler using that code set
would raise with the cause obscured.

Fixed at the SINK, not at the file producer. `_load_database_source` routes its
cells through `_cell` while `_load_file_source` returns `dict(load_code_set(
path))` uncoerced: two of three serialization boundaries were hardened and the
third was not. Coercing the file producer fixes this instance; a `default=` hook
on the sink fixes the class, including the producer nobody has written yet.

`encode_reference_value` lives in `store/metadata.py` -- the existing home for
pure helpers shared by all three backends "so the merge can never drift". It
carries the same coercions as `transports/database.py::_json_default` (dates to
ISO-8601, Decimal to its exact string, bytes to base64) because a reader cannot
tell a FILE-sourced snapshot value from a DATABASE-sourced one, so the two must
not disagree; `transports/` may not import `store/` (ADR 0154 AC-17), so a test
freezes the agreement instead. An unencodable type still raises TypeError --
never accept-and-drop.

15 tests, each confirmed red first against the pre-fix code. The SQLite arms go
through the real store and the full ReferenceSyncRunner. The SQL Server and
Postgres arms drive their real `write_reference_snapshot` with a pool whose
acquire raises a sentinel: both build the encrypted row list BEFORE acquiring a
connection, so the encode step is reachable with no server running -- which
matters because a local pytest silently skips both DB legs. Reverting each
backend independently reds only its own arms.

Every existing reference test uses CSV, where all values are already str, so the
suite was structurally incapable of reaching this.
…oosening registry (#333)

Steps 3 through 5 and 7. Under ADR 0148 (one posture, loosen only) a deviation
the registry cannot see is a second posture by the back door, and both of these
were invisible: tls_allow_expired appeared in NONE of config/settings.py,
api/app.py, checks.py or __main__.py, and the generic-ODBC DATABASE hop's whole
control was a construction log line.

Two readers beside accepted_cleartext_hops -- expiry_relaxed_hops (the flag
lands in spec.settings, not a typed field) and unverified_generic_db_hops, which
walks INBOUND as well as outbound because a DatabasePoll crosses the same hop
with the same credential in the same DSN. The DB reader imports the step-1
classifier rather than restating it, so a surface can never report a hop the log
warns about as clean. Peer labels mask URL userinfo and render an unresolved
env() as its key, since they land in GET /security/posture.

security_loosenings takes both as REQUIRED parameters, per its own docstring: an
optional parameter is a detector that silently fails to fire. All four callers
updated. The expiry risk text states BOTH halves -- accepted indefinitely with
nothing that expires it, chain/hostname/key-usage still verified -- because
stating only one would either overstate it into verify-off or hide that a lapsed
bridge never closes itself.

Both graphless callers now name all three connection-scoped deviations in their
scope marker; naming only cleartext_accepted made the DECLARED scope itself
incomplete.

Step 7, the durable half: the existing floors iterate model_fields, so a
connection-scoped deviation is outside their reach BY CONSTRUCTION. The new
floor censuses the connection FACTORY signatures instead -- every TLS-shaped
parameter must be reported by a reader or exempt with a written reason. Proven
red on purpose twice: an injected unclassified parameter (message prints all 15
factories scanned and every parameter, not a count), and a renamed parameter
tripping the blindness guard.
…tions (#333)

Step 6. Ten assertion sites, not the five the item predicted -- enumerated by
grep against the shipped file rather than trusting the count: seven
tls_allow_expired occurrences in DEPLOYMENT.md plus three generic-ODBC sites.
Eight became outright false the moment the registry entries landed and two
needed tightening.

The false ones all said some form of "nothing reports this": the step-5 checklist
("no posture gate, escape variable or loosening register covers"), the matrix row
and the matrix exceptions bullet, the tls_allow_expired section's "absent from
security_loosenings(), and therefore from GET /security/posture ... nothing
reports that a connection has it set", the MEFOR_ALLOW_INSECURE_TLS section's
"no environment variable, posture clamp or loosening register covers it at all",
and the maintenance note's "security_loosenings() never reports it".

Two tightenings the item called for and one it did not. "with nothing refusing
it, warning at posture level, or reporting it" overreached against the
construction WARNING the same section acknowledged four lines earlier -- now it
claims only what is true, that nothing refuses it. "nothing that expires it or
surfaces it" conflated two different facts: it IS surfaced now, and it is still
never expired, so the sentence splits and the risk-register advice narrows from
"the engine will not keep that list" to "the engine keeps which connections, not
until when". The maintenance note gains the standing rule the whole edit turns
on: reported is not gated, and neither implies the other.

The section heading stays "the weakening with no posture gate at all" -- that is
a claim about a GATE and it is still exactly true.

SECURITY-LOOSENING.md gains what it mentioned neither of: two table rows, two
full entry sections, two standards-mapping rows, and a scope note for the second
completeness floor. The generic-ODBC entry records the encrypted-but-unverified
residual (sslmode=require) rather than implying the detector grades cipher policy.
…d's VALUE (#333)

Made false by the step-1 detector fix in the same branch: the note said the
WARNING drops to DEBUG "once a TLS keyword is set", which was the value-blind
behaviour. It drops only for a keyword set OUTSIDE the no-TLS deny-list, the
line now names the connection, and the delegation is reported on the posture
surfaces as well as logged.
The expiry reader already states that FhirLookup and the inbounds carry no
tls_allow_expired, so adding it later cannot silently escape it. The generic-ODBC
reader owed the same sentence and did not have it. Verified rather than assumed:
neither DatabaseLookup nor DatabaseRef takes a dialect or odbc_params parameter,
and the ADR 0010 read executor calls _build_dsn directly, so a live lookup is
SQL-Server-only and keeps that preset's posture-keyed refusal.
…(#1040, #1035, #1076, #1036)

A deny reason is not a log line. It carries a command block a model is told to run, built by
interpolating values that model's counterparty chose. The gate had one helper for values entering
PROSE and none for values entering a COMMAND, so the command class was handled per site -- and per
site is how one line ended up quoted and the line below it bare.

TWO HELPERS, ONE PER CLASS, plus a backstop under both:

  Get-SafeForMessage   folds CR/LF/TAB and caps length -- for a value entering PROSE, where the
                       exposure is forged line structure (a crafted path produced a reason with two
                       "Do this instead:" blocks, the forged one first).
  Get-SafeForCommand   folds, then wraps in single quotes with interior quotes doubled -- for a value
                       entering a COMMAND, where the exposure is execution. $Prefix/$Suffix compose
                       inside the quotes because pwsh's argument parser splits `'main':README.md` into
                       TWO arguments where bash makes one, measured.
  Protect-CommandLines runs over every reason at Write-Deny, the one funnel every rule already passes
                       through, and drops shell metacharacters that sit OUTSIDE a single-quoted span
                       on an indented pwsh/git line. A helper-produced value is inside quotes and is
                       untouched, so this cannot make a correct line wrong -- it only defangs a line
                       that did not use the helper, which is the failure that actually recurs.

QUOTING IS THE FIX AND FOLDING IS NOT. Stripping the metacharacter would emit a command naming a
branch that does not exist, which is the unrunnable-remediation defect of #1032 arriving from the
other side. Measured on a branch named `pwn$(hostname)`: bare, both pwsh and bash execute the
substitution; single-quoted, both yield the literal refname and the command still runs.

#1035 -- every `pwsh -NoProfile -File <path>` the gate prints (rules 1, 2, 3, 3b, 3d, 4; nine
emissions) now quotes its path, as do rule 3's `git -C` plumbing lines. Measured against a primary at
`<tmp>/Pri mary`: unquoted, pwsh exits 64 with "The argument '<...>/Pri' is not recognized as the
name of a script file"; quoted, it exits 0 and the named script runs.

#1076 -- rule 3b's READ remediation emitted $dest and $selfTopRaw BARE one line under the quoting
that fixed the same class. Both are now quoted, and a test asserts the emitted STRING against a
hostile refname rather than merely that the call denied.

#1036 -- rule 4 fires on the tool name alone, so it had no path to key on and named $roots[0]
whichever repo the session was in. It now resolves the session's own governed root: by prefix for the
primary and its nested worktrees, and via `rev-parse --git-common-dir` for a sibling worktree. When
neither answers it says so and prints no runnable command, because a path that exists and runs
against an unrelated clone is worse than no remedy at all.

EVIDENCE. tests/test_worktree_gate_emitter.py drives every rule that emits a command block and runs
what it printed against stub scripts, asserting the named script ACTUALLY RAN rather than that a
string looked right. Against the pre-fix gate 25 of its 28 cases are RED; the 3 that stay green are
the narrowness cases (the scanner's own positive control, and rule 4 from the first allowlist entry,
which is the one shape $roots[0] got right). The harness itself was caught green-on-nothing first: a
missing function was a non-terminating error, so three cases measured an empty string -- it now fails
closed on both.

_remove_ps1_targets in the remedy-families module gained the optional quote it now has to skip;
without it the pattern matches NOTHING and every assertion built on it goes vacuously green.
…s NOT list these (#333)

Both new entries said the deviation reaches "the serve-time loosening warning".
It does not, and cannot: that warning fires before the Engine loads the graph
inside the ASGI lifespan, so it passes empty tuples for all three
connection-scoped deviations -- verified at the call site, not assumed. The
SECURITY-LOOSENING.md entry even contradicted itself two bullets later, where it
points at cleartext_accepted's identical not-reported-here list.

Naming a surface that does not carry the finding is the same defect class the
item exists to fix, one level up: a reader who audits the serve log and sees
nothing would conclude no connection has it set.
…web console step

Two halves of #344, built together because the second is what makes the first
measurable on both gated steps.

MARGIN CHECK (proposal 1). scripts/ci/step_margin.py runs after the two gated steps in
`test` and reds the leg when a step's own duration comes within 1.30x of its own cap.
The three traps the item names are each closed in code rather than in a comment:

  * it times the STEP, never the job -- it is handed one step's elapsed and one step's
    cap and cannot see a job;
  * it keys on `steps.<id>.outcome`, the step's OWN conclusion. Filtering on the job
    deletes the tightest rows by construction and reproduced a published maximum of
    24:35 where the truth was 25:51;
  * a recorded maximum whose pool was censored is a LOWER bound, so
    step_margin_baseline.toml records per-(step x leg) maxima with their pool, their
    date and a censored flag, and the check prints the caveat instead of dividing by it
    quietly. A run exceeding the record prints RE-DERIVE with the pool attached -- that
    table was published wrong twice and nothing read it.

Four outcomes, four behaviours: OK; LOW (exit 1, the gate); CENSORED, where the step did
not conclude success so its elapsed is a lower bound and NO margin is claimed; and
NO OBSERVATION, where the step was skipped -- said in words, because a docs-only leg
reading as a healthy margin is the negative control this check would otherwise fail. A
capped step with no baseline row fails CLOSED (exit 2).

The check runs its own red/green control pair on EVERY invocation and prints both into
the job summary, and refuses (exit 2) if either arm disagrees. A gate that has never been
red is a claim, not a control, and this gate's red condition has never occurred here.

The clock is marked through the script rather than `echo ... >> "$GITHUB_ENV"`: that
spelling routes a Windows path through a bash redirection on two of three legs, which no
local run can exercise and whose failure mode is a silently absent variable. A missing
mark is a refusal, never a zero.

WEB CONSOLE CAP (proposal 5). That step carried `matrix.step_timeout` -- a 2-to-4-minute
suite behind a 25-to-55-minute budget -- so its cap could never fire first and a hang
there was an unattributed job-level kill with no step conclusion. It now carries
`matrix.webconsole_step_timeout` (ubuntu 5, Windows 6), sized by the same 1.35x rule
floored at 5:00. The nesting invariant setup(max) + step_timeout + webconsole_step_timeout
< job_timeout now HOLDS on all three legs -- 35:22/37:00, 65:05/66:00, 63:32/66:00 -- where
it previously held on none, and the worst case the caps admit is finite and positive for
the first time. job_timeout is unchanged: capping a step does not change how long it takes.

The ci.yml paragraphs asserting the gap is open are corrected in the same commit rather
than left to contradict the file below them.

EVIDENCE. Every check was made to go red on purpose first:
  * the margin gate at 25:51 against the retired 26:00 cap prints
    "MARGIN LOW: 25:51 of a 26:00 cap (99.4% of the cap, margin 1.006x, floor 1.30x)"
    and exits 1 -- the run this item was filed for, flagged;
  * putting the web console step back on `matrix.step_timeout`, keying the check on
    `job.status`, or dropping `webconsole_step_timeout` from a leg each red a named test;
  * moving the check above the web console step reds the ordering test;
  * a floor of 0.0 (a gate that cannot refuse) makes the live control raise and exit 2.
actionlint is clean, and was proven to SEE this file by injecting an invalid expression
into the margin step and watching it report ci.yml:725.
A `max_passing = "0:00"` row divides by zero on the percent-of-record line, and a row that
records "no observation yet" as "zero seconds" is a row asserting the suite has never run.
Refused at load, where the offending row is still nameable, rather than surfacing as a
ZeroDivisionError from inside a note.
…es (#1040)

#1040's closing claim was that the deny surface is not confined to the worktree gate -- "every hook in
scripts/hooks/ that emits a remediation an agent is told to run has this shape". Measured on this one:
a PreToolUse payload whose file_path carried embedded newlines produced a notice with TWO "Before
overriding:" blocks, the FORGED one FIRST, with a command of the caller's choosing where the real
overlap.ps1 line belongs. Nothing has to exist on disk -- only the JSON field -- so this is the same
instance-two shape found in the worktree gate's rule 1b, in a second file.

The values are not only the path. Branch and Worktree come from overlap.ps1, so a refname is
attacker-choosable from a public fork, and Work is free text from the session registry. All of them
now fold, because deciding value by value is what left the last one bare.

PROSE ONLY, and that is a statement about this file rather than a general rule: every command it
prints is a literal with no interpolation, so there is no command-bound value here and no quoting
helper. If a command line here ever gains one, folding is NOT the treatment -- see the worktree
gate's Get-SafeForCommand.

The fold is a LOCAL COPY rather than a shared import, deliberately: worktree_gate.ps1 is installed
OUTSIDE every working tree, so it can dot-source nothing from a checkout, and a shared module would
be importable by one of the two hooks and not the other -- two definitions of one rule that drift
invisibly. Four duplicated lines with the divergence visible to grep beats that.

EVIDENCE. Both new cases are RED against the pre-fix gate and green after, and the assertion is on
STRUCTURE -- the refusal has the same number of lines as the benign one -- which a different payload
cannot slip past. A non-vacuity case pins that the value is still SHOWN: a gate that hides what it
blocked trains people to route around it. The line-count spelling was reached after two narrower ones
were found to answer an adjacent question, and the reasoning is recorded beside the test.
`[store].connect_timeout` bounds the LOGIN and `[store].command_timeout` the
STATEMENT. Neither bounds the WAIT for a free pooled connection, and that wait
was unbounded at three sites:

  store/sqlserver.py::SqlServerStore._acquire   -- the single chokepoint every
                                                   store DB call funnels through
  store/postgres.py::PostgresStore._timed_acquire
  pipeline/reference_sync.py::_load_database_source (the DatabaseRef pool)

On a first deployment against a server backend, a pool-exhausted or unresponsive
database would block the acquiring task forever with the queue backing up behind
it -- unlike the DATABASE connector borrow, which `acquire_timeout` already
capped at 30 s.

New `[store].acquire_timeout` (default 30.0, must be > 0 -- no "0 disables",
because an unbounded pool wait is what the setting exists to remove), plus
`DatabaseRef(acquire_timeout=...)` for the reference source. One shared helper,
`store/base.py::acquire_pooled`, so the two backends cannot drift on what
happens at the limit.

A FOURTH site was found while measuring and is fixed too: Postgres's
`_fetchall`/`_fetchone`/`_execute` called `self._pool.fetch/fetchrow/execute`,
each of which acquires INTERNALLY with no timeout (asyncpg 0.31.0
pool.py:613-634). They now route through `_timed_acquire(record=False)`, and the
low-frequency status polls still stay out of the B11 worker acquire-wait curve.

COVERAGE IS NOT UNIFORM, AND THE DOCS NOW SAY SO. An absence check with a live
positive control refuted the claim this commit was first written with. Measured
2026-08-10: SQL Server's `_acquire` really is the sole borrow site (the other
seven `self._pool.` uses are close/wait_closed/maxsize/size/freesize/release), so
that backend is fully bounded. Postgres is NOT -- 38 direct
`await self._pool.<verb>(...)` calls remain on the auth, session, audit,
retention and attachment paths, plus 10 in pipeline/cluster.py on the same pool,
each acquiring inside asyncpg with no timeout. Those are outside this item (it is
about the pipeline stalling) and are reported as a residual rather than described
away. CONNECTIONS.md and CONFIGURATION.md state the per-backend split explicitly:
on Postgres a wedged pool bounds the pipeline but can still stall an API/auth
read.

Two scan gates pin that split so it cannot rot, and BOTH were made to fail on
purpose: injecting one `self._pool.acquire()` outside `_acquire` reds the SQL
Server gate, and reverting `_fetchall` to the direct pool call moves the Postgres
residual 38 -> 39 and reds the other. They report the LINES they found, not a
count. The scan requires an `await`/`async with` prefix -- without it, it also
matched two docstring mentions and reported 40 where there are 38, which is the
instrument answering "does this text appear" rather than "is a connection
borrowed here".

Shield-then-cancel-then-salvage, not a bare `wait_for`. A bound that abandons a
borrow mid-flight would slowly leak the very resource it protects: the pool
marks a connection in-use before handing it over, so an abandoned borrow strands
a connection nobody holds and nobody can return, shrinking a pool that is
already wedged by one slot per retry. `asyncio.wait_for` cannot avoid that -- on
expiry it cancels the borrow, and a cancellation landing in the same loop
iteration the borrow resolves discards the connection. `warm_pool_connections`
already records this leak-freedom invariant for its own acquire side; this makes
it explicit for the bounded one. A caller cancellation takes the same path, or a
shutdown mid-borrow would strand a slot across a failover flap.

`StoreAcquireTimeout` is an ordinary Exception so it lands in every store
caller's existing `except Exception` and is treated as a transient stage failure
(row stays claimable, handoff re-runs idempotently). Deliberately NOT a
TimeoutError -- since 3.11 that is an OSError subclass and connector-error
handling reads OSError as a network fault.

ADR 0159 is preserved exactly: `async with pool.acquire()` became an explicit
acquire/release only so the bound can sit between the two, and aioodbc's context
manager does nothing on exit but `await pool.release(conn)` (0.5.0
utils.py:86-103). The quarantine still runs BEFORE the release, which is what
makes a poisoned connection unlendable. The #348 fake pool is re-modelled on the
driver's acquire/release PAIR -- strictly closer to the real pool than the
_ContextManager wrapper it replaces, and its release rule is unchanged.

22 tests, each confirmed red first. STORE-LEG COVERAGE: the SQL Server and
Postgres legs are SKIPPED by a local pytest, so every arm here is driverless --
both stores built with `__new__` over a fake pool, the existing idiom from
test_backlog348. The DatabaseRef arm asserts the bound by wrapping `sync_all()`
in an outer wait_for: pre-fix it never returns.
…dies (#1040)

Found by inventorying the emitted lines rather than by a test, which is the point of recording it.
Rule 1b's alloc remedy is a LITERAL carrying the placeholder `-Kind <adr|backlog>`. A '|' on a
command-form line is a pipe in both shells, so Protect-CommandLines dropped it -- correctly, since it
cannot tell an author's placeholder from an injected separator -- and the remedy went out as
`-Kind <adrbacklog>`. Measured against the previous commit; nothing in the suite saw it, because
every existing assertion was on the DECISION or on a substring that did not span the placeholder.

The placeholder is now spelled `<adr-or-backlog>`. A placeholder can always avoid a metacharacter; a
backstop with an exception carved into it for one is not a backstop, and carving the exception is how
`<$(...)>` would then walk through -- bash expands a command substitution inside a redirect target.

The general guard is the new part. Rule 1b's table is READ OUT OF THE GATE SOURCE and every entry's
remedy is asserted to appear byte-for-byte in the deny that arms it, so any future collision between
a literal remedy and the sweep fails at the site that caused it rather than shipping as a quietly
mangled command. Reverting only the placeholder turns that case RED and leaves the other six green.
… PID set (#1210 arm 4)

`_derive_sets` pinned `handles=61` and `working_set_bytes=6_000_000` on every tick regardless of the
`cpu_pids` it was varying. That input shape is physically unrealizable: on Windows the probe reads
handles, CPU and working set from the SAME `Get-Process` rows, so a PID joining the sum necessarily
moves all three. It is also the one shape in which an over-wide subtree is invisible -- and
`test_a_membership_changed_interval_is_degraded_to_a_gap` then asserted that pass-through as CORRECT,
with a rationale ("the process set was observed") that is true of the differencing arithmetic and
false of a peak over a set never validated to be the engine's.

Derive both gauges from each tick's PID set instead, so no test can pin them apart again, and
re-express the two assert sites against the derived values. The membership-change test now reads the
wider middle tick, which is the honest answer for an instantaneous gauge and makes explicit that a
BACKLOG #220-shaped gate cannot establish provenance -- the walk has to.

Red-first evidence, this commit's assertions run against the pre-fix pinned fixture:

    assert d.handles_peak == _HANDLES_PER_PID * 2
    E   assert 61 == (61 * 2)
    E    +  where 61 = _ProcDerived(handles_peak=61, ..., working_set_peak_bytes=6000000).handles_peak

Tests-only. No production code, no SLO, no threshold, no report schema.

BACKLOG #1210 (arm 4).
… not the FD/RSS peak (#1210 arm 1)

`_walk_descendants` BFSed the ppid map with a PID cycle guard and nothing else -- no creation time, no
image name, no cardinality bound. Windows does not rewrite `ParentProcessId` when a parent exits and it
recycles PIDs, so any live process whose recorded parent PID is later reissued to the engine root is
adopted along with its whole subtree; its handles and RSS are summed into `fd_count_peak` /
`working_set_peak_bytes`, and `max()` latches the result for the sweep step. A merge-blocking SLO then
draws a verdict from it -- which is how a scorecard-writer PR touching two `scripts/asvs/` files went
red on `fd_count_monotonic`.

The fix validates the WALK rather than gating the aggregate: a genuine descendant cannot predate its
root, because the parent must already exist to create the child. The Windows enumeration now projects
`CreationDate` alongside ProcessId/ParentProcessId (as UTC .NET ticks, so a locale, a null on the
Idle/System pseudo-processes, or a DST transition mid-run cannot corrupt the ordering); POSIX reads
/proc/<pid>/stat field 22 (starttime, ticks since boot -- one origin for the whole host). Any candidate
that started more than `_CREATION_SKEW_TOLERANCE_S` before the root is rejected, and its subtree is
pruned rather than re-entered: if a node is not ours, its children are not ours either, and re-entering
them is what produced the five-figure sums.

Two fail-closed choices, both tested. A candidate with no recorded creation instant is rejected --
unvalidatable is not validated. A snapshot with no row for the ROOT reports "cannot resolve", which the
Windows caller already turns into a degraded gap plus a retry, rather than walking unchecked. The
one-second tolerance absorbs clock granularity (~15.6 ms on Windows, SC_CLK_TCK on POSIX), not age: an
adopted subtree is old by construction, because its real parent had to exit and the PID space had to
wrap before the root could be issued that PID. It is one-sided -- it only ever ADMITS -- so it is not
zero, and a genuine child inside it is asserted as a positive control.

Structure: `_enumerate_windows` / `_enumerate_posix` now return `(pid, ppid, created_s)` rows and
`_validated_descendants` does the checked walk, which also gives the acceptance test a seam that
simulates only the OS's stale ppid link.

ACCEPTANCE, measured, not a helper assertion. Adoptee spawned first holding 200 sockets, then the root;
the adoptee's recorded parent re-pointed at the root, which is exactly what Windows reports after a PID
recycle. Every other input is the OS's own.

    PRE-#1210  subtree: [6288, 47944, 34472, 42924] -> 496 handles
    POST-#1210 subtree: [6288, 34472]               -> 144 handles
    adoptee subtree alone                           -> 352 handles   (496 - 144)

The same shape on a real Linux /proc (WSL2 Ubuntu, python3.12, SC_CLK_TCK=100), so field 22 is
exercised and not merely reasoned about:

    PRE-#1210  subtree: [413, 412] -> 206 fds
    POST-#1210 subtree: [413]      ->   3 fds

Red-first: with the walk body reverted to its pre-#1210 unvalidated form the acceptance test fails at
the right assertion, after its positive control confirms the adoption is real --
`AssertionError: (43748, [46672, 43748, 356, 48060])`.

Also corrects a docstring premise. The module asserted that `messagefoundry serve` runs the uvicorn
engine as a child on Windows. It does not: `uvicorn.run` is in-process and children come only from the
`--shards` supervisor. The real cause of the thin root is the venv `Scripts/python.exe` launcher shim
re-execing the base interpreter, which is a property of the LAUNCHING interpreter, so whether the root
is thin is environment-dependent. Measured 2026-08-10 on a stdlib venv over a pythoncore-3.14 install:
root 61 handles / 6.55 MB, its re-exec child 141 handles / 15.2 MB -- the conclusion holds there, the
stated cause did not.

No SLO, no threshold, no report schema, no runner change. Arm 2 (record the covering PID set on the
report) is a separate item; arm 3 (a cardinality gate) was rejected during the ruling.

BACKLOG #1210 (arm 1).
…checked

The corrected docstring still carried an enumeration presented as complete -- "children otherwise come
only from the `--shards` supervisor". A completeness claim is a liability (Code_Quality/SDS-3.6), and
this file is where the previous topology claim went wrong in exactly that way: it asserted a shape the
code did not have, and re-reading it only confirmed the error.

Restate it as what was actually checked -- the child is not `serve` spawning uvicorn, `uvicorn.run` is
in-process, and the child-spawning path in `serve` is AT LEAST the `--shards` supervisor, which the
connscale smoke does not use. Prose only.

BACKLOG #1210 (arm 1, follow-up).
…cklog numbers (#1033)

docs/Code_Quality_Standards.md cited its own eleven rubric signals as bare `#N`.
In this corpus a short `#N` reads as a backlog item, and six of the seven
distinct numbers used here resolve to real, unrelated items. Convert every one
to the `signal N` form the file already uses elsewhere.

Ten citations on four lines, re-measured against origin/main (516f59e) rather
than the line anchors the item recorded against 780ee1d, which had drifted +2:

  L284  #10
  L301  #7, \#8, \#9, \#11, \#10
  L321  #10
  L422  #6, #7, #9

The escaped shapes are why the count is measured and not grepped: a pattern
requiring a space or paren before `#` misses `\#8`. The census pattern carries
four positive and five negative controls and refuses to report until they pass.
That self-test earned its keep -- the first draft's word-char lookbehind made it
blind to the L120 anchor fragment, the one token that must be left alone.

The L422 numbers are pre-0.6 signal IDs, so that row now says so; leaving them
bare would have read as current IDs after the 0.6 renumber moved them.

Measured after the change: short `#N` tokens 11 -> 1, the survivor being the
L120 anchor `Secure_AI_Development_Standards.md#3-the-problem-this-standard-attacks`,
untouched. Four-digit PR citations 40 marked, 0 bare. Markdown link targets
changed by this edit: 0 of 57, via a differ proven live by an injected change.
…planned; both ship (BACKLOG #1053)

The Logs section described structured (JSON) logging and off-box syslog/SIEM forwarding as
planned and bundled with off-box exposure. Both are built at HEAD and verified in the code
before this edit: JsonFormatter and SyslogForward in messagefoundry/logging_setup.py, and
LoggingSettings.format / forward_host / forward_protocol / forward_format in
messagefoundry/config/settings.py, where forward_format already defaults to JSON and naming a
forward_host turns forwarding on by default (ADR 0080).

The section now says both are built and names the settings that arm them, plus the attestation
gate a plaintext collector hop meets on an enforcing production-PHI instance -- the one that
decides whether the engine starts. The full [logging] table is not restated here: CONFIGURATION.md
is the settings record and already marks this work done, so this links to it (SDS-3.5).

The DEBUG warning is kept and widened -- it was previously worded as a stopgap until structured
logging arrived, which read as though it expired when it did not.
…its on: block declares (BACKLOG #1079)

The header carried a paragraph asserting there is no push-to-main trigger, with costings, while
'push: branches: [main]' sat ten lines beneath it -- and the push arm's own comment gives the
reason that denial ignored: a fork PR is scanned structural-only because the secret is unavailable
to it, so without that arm no fully-loaded scan ever sees fork-contributed content. The on: block
is the behaviour; the header was the stale half.

DELETED rather than softened. Correcting the paragraph would have left a second definition of the
trigger set in place, free to drift again. The replacement text states only that the on: block is
the single definition and why the header keeps out of it. The on: block itself is untouched.

The cost of the drift was never to CI, which behaved as the on: block says. It is that the rest of
that header is load-bearing -- it is where the continue-on-error trap is documented -- and a reader
who finds one paragraph demonstrably false cannot tell which of the others still hold.

Guarded by test_the_security_header_does_not_contradict_its_own_triggers in the module that already
owns every other security.yml assertion. Confirmed RED against the pre-fix header first (matched
'NO push' at offset 1619 and named the 'push' event from the parsed on: block). Non-vacuous three
ways: the header is located by construct and asserted substantial, the event set is read from the
on: block (handling the YAML 1.1 'on' -> True key) and asserted non-empty, and the detector is
fired against the historical claim in the same run, so its silence on the current header is
evidence rather than an assumption. Its scope is stated in the test: a tripwire on the shape that
occurred, not a proof that English agrees with YAML.

Master test plan chapter 16 finding 4 is rewritten as closed and SEC-72 records the security.yml
half as built while naming the codeql.yml/scorecard.yml half that is still open -- the row is not
closed by this. Both edits ride the same commit that closes the weakness they describe.
… file its item lives in (BACKLOG #1095)

Retiring an item moves it verbatim from docs/BACKLOG.md into docs/archive/backlog/, and every
citation that named the live file keeps pointing at a file the item is no longer in. No link
checker can see this class: docs/BACKLOG.md resolves perfectly and only the human-readable number
beside it is stale. link_check.py and this ask different questions; neither subsumes the other.

RESOLUTION IS AGAINST ONE NAMESPACE, COMPUTED AT RUN TIME. scripts/docs/backlog_citation_check.py
imports parse_items and DEFAULT_SOURCES from backlog_status_check rather than reimplementing either
-- CLAUDE.md section 11 makes parse_items the single definition of item location, and adding an
archive file stays the one edit that module documents. Nothing here encodes an item count, a
per-file total, or which file a number lives in: wave 1 moved 41 items in a single pass, so any such
figure would have been stale before it landed. test_a_citation_resolves_identically_wherever_its_
item_lives moves an item between the two files and asserts the verdicts SWAP with no edit to the
citing document and none to the checker -- that is the property, and it is what would break first if
an assumption were ever baked in.

WHAT COUNTS AS A CITATION is a number BOUND to a ledger path by construct -- in the link text, in the
link fragment, or abutting the link with only whitespace or light punctuation between. There is no
proximity window, because a window is a tolerance and a tolerance decays. A same-line rule was tried
first and measured: it reported prose that names the ledger files generically on a line that happens
to mention an unrelated number, which is a common shape in BACKLOG.md itself. That false positive is
now a pinned test.

DIFF-SCOPED, which is the surviving objection from PR #271 answered rather than waived: a gate red on
day one over pre-existing violations gets suppressed, so --base/--head restricts findings to lines
the PR ADDED, and the gate can only be red about something the PR wrote. Run with neither flag for
the repo-wide report, which is a measurement rather than a merge gate.

A NUMBER THE NAMESPACE DOES NOT CARRY WARNS AND DOES NOT FAIL. docs/BACKLOG.md says of itself that it
is a published baseline of a fuller maintainer-internal ledger. Measured 2026-08-10, the
unresolvable citations are #13, #270 and #287 -- real items behind that publishing boundary, none of
them broken. Failing there would be red for a reason no contributor can fix. It is still reported,
because the same class catches a mistyped number.

The check rides the existing backlog-hygiene job as a second step rather than a new job: the job
name is the branch-protection context string, so it cannot be renamed to describe both checks
without wedging every PR, and a new context would be unrequired and therefore decoration. The
workflow header now says both of those things.

Anti-narrowing runs the opposite way to the status gate, so it needs no --min-items floor (which
would itself be the hard-coded count this module must not carry): read fewer ledger files and every
citation of the missing items turns red at once. An empty namespace is refused by name, and the
per-file item split is printed on every run.
…chived item (BACKLOG #1095)

Every site the new checker reports outside the ledger files themselves, repaired in one act rather
than a subset -- #1095 is explicit that repointing some sites asserts by contrast that the untouched
siblings are still live, and uniform staleness is at least detectable where a confident wrong
pointer is not.

Found by running scripts/docs/backlog_citation_check.py repo-wide (272 ledger citations across 347
markdown files, namespace 477 items = 241 live + 236 archived, measured 2026-08-10):

  docs/CONFIGURATION.md:130                     #235 -> archive
  docs/CONFIGURATION.md:512                     #235 -> archive
  docs/adr/0006-external-data-lookups.md:180    #235 -> archive
  docs/adr/0068-...-offloopback.md:10           #11  -> archive
  docs/adr/0113-windows-tray-service-manager... #239 -> archive
  docs/research/openflow-step-attributes.md:5   #238 -> archive

ADR 0068's site was the 'adjacent' construct, [BACKLOG](../BACKLOG.md) #11, and is rewritten to
BACKLOG [#11](...) so the number carries its own link like its #75 sibling on the same line -- that
line cited two archived items and now names the archive for both.

No fragment is invented anywhere. #1095 carries #1094's warning that a hand-written anchor is worse
than none because it looks precise and lands nowhere, and two independent derivations of the slug
rule disagreed during that work. The paths are repointed; existing fragments are left as they were.

Two further sites remain, both INSIDE docs/archive/backlog/BACKLOG-CLOSED.md, and are left for the
ledger owner rather than edited here.
…s (#1092)

The quality record scored signals "machine-checked"/"enforced" without naming
the instrument behind each claim or that instrument's measured scope. A gate's
name is a claim; only its measured output and scope are evidence. Adds section
4.0 rule 4 (the scope rule, sibling to the liveness rule) and Appendix A.5, the
per-instrument scope register.

The substantive correction is signal 1. `tests/test_dependency_boundaries.py`
was cited for an unqualified "import/layer rules are machine-checked in CI".
Measured by mutation rather than by reading it -- a planted `import fastapi`:

  transports/, store/                         RED   (gate sees it)
  auth/, anon/, checks.py                     GREEN (never scanned)
  harness/, tee/, scripts/                    GREEN (never opened)

So the engine's one-way rule is genuinely enforced and the row stays Strong for
it, but the client-side layering convention the sentence appeared to cover has
no instrument. The gate is sound; the prose beside it was not.

The same mutation pass found the opposite error in a prior description of that
gate: it resolves `ast.ImportFrom` and relative imports, not `ast.Import` alone,
and `test_relative_imports_are_resolved_not_skipped` pins that. An understated
scope is the same defect as an overstated one, which is why A.5 records scope
established in both directions rather than read off the source.

Every other count was re-measured 2026-08-10 and every one was stale in the
same direction: test functions 5,402 -> 9,706; pytest.raises ~1,000 -> 1,608;
SECURITY.md 735 -> 1,849 lines; PHI.md 688 -> 1,335; C901 122/43 files ->
132/46. Dates now attach to the figures, not to the appendix. Signal 2's "no
blanket ignores" stands once scoped -- zero inside the mypy-checked tree; the
two in the repo are both in tests/, which CI does not type-check.

tests/test_quality_record_scope_claims.py pins the register against the gate's
own `_ENGINE_PACKAGES`, so widening the gate without updating A.5 is red rather
than silent drift. It also pins the #1033 `signal N` convention and the L120
anchor. Each guard was verified red-first against the mutation it exists to
catch; two of them could NOT fail on the first attempt -- both searched too wide
a span and stayed green while the fact they assert was deleted from the row that
carries it. Both were narrowed until the mutation went red.

No scoring change: A- stands, still 11 signals. This corrects the record, not
the controls.
…cker sees (BACKLOG #1095)

The two checkers use different regexes on purpose. link_check starts at '](' because it only needs
the href; the citation gate must capture the display TEXT to read a number out of it, so it
additionally requires a well-formed '[text]' and would silently skip any ledger link whose text
carries a bracket. Without this control, 'no citation defects found' would be a statement about the
regex rather than about the docs -- the exact shape of a green that is not evidence.

Measured 2026-08-10: the two see the SAME 191 ledger links repo-wide, zero seen by one and missed by
the other. The test asserts that stays zero, and carries a >100 floor so a parity assertion cannot
pass over an empty scan.

Confirmed able to go RED: narrowing the citation regex by one character class (excluding a backtick
from the link text) made 34 ledger links invisible to the gate and the test named all 34.

.github/required-contexts.txt records why the backlog-hygiene context string now under-describes its
job: the job also runs the citation gate, it rides that context because a new one would be
unrequired and therefore decoration, and the name cannot be changed to describe both without
becoming the required-but-absent trap.
…ing tree (BACKLOG #1095)

Line numbers come from 'git diff BASE...HEAD'; the content was coming from the checkout. On a
pull_request event actions/checkout lands the MERGE ref -- base merged with head -- not the head
commit, so a file that moved on both sides is a different file from the one the diff measured, and
the finding names a line that is not the line. Confirm the instrument answers the question you
asked: content and line numbers must come from the same revision.

The failure direction is the bad one. Confirmed RED with the pre-fix line restored: with the
citation pushed from line 1 to line 6 in the tree, the gate printed 'ledger citations in scope: 0'
and 'OK' over a real violation -- a green that is not evidence, in a gate whose entire purpose is to
be evidence. tests/test_backlog_citation_check.py::test_diff_scope_reads_the_file_at_head_not_the_
working_tree reproduces the divergence and pins it.

Repo-wide mode still reads the working tree, deliberately: there the question IS what is checked
out.
…rom one probe (#1103)

run_connscale binds engine_api_port_base + step for every sweep step, but nothing
stated how many steps that is, so every caller reserved one port and assumed the
rest. sweep_step_count() is now the single definition of the loop cardinality, and
the loop checks its own step index against it: a sweep that grows an axis without
teaching the function about it fails loudly on the first unreserved port instead of
binding it and dying inside uvicorn with an errno that names no port problem
(EADDRINUSE on Linux, the far less obvious "access forbidden" 10013 on Windows).

A pooled-arm miss consumes a step and abandons that cell's remaining trials, so the
real step count can be lower than this number but never higher, which is what makes
it a safe reservation width.
…blocks (#1103)

#1014 gave the inbound family a real reservation -- probe a contiguous run, random
anchor, contiguity asserted at acquisition, fail loud rather than fall back. The API
and sink families never got it: each base came from one bind(("127.0.0.1", 0)) that
was closed before it returned, so exactly one port of each range was verified.

tests/_connscale_ports.py is now the single definition all three families share. The
windows are disjoint and all sit below the OS ephemeral floors, so the kernel cannot
hand out a port inside a block after it is probed -- a verified-then-released
ephemeral port was never worth much, since the kernel allocates from that same range.
Measured over tests/, harness/, samples/, packaging/, messagefoundry/, ide/, scripts/
and .github/: [20000, 32700) carries no fixed port bind in the tree.

Every new check was made to fail on purpose first. Reducing the allocator to a
base-only probe reds both every-port guards; striding the runner past its reserved
range reds the coverage test; disabling the cardinality guard reds its own test; and
overlapping two windows reds the disjointness test. The first draft of the dodge test
used the full 700-port window and survived the base-only mutation green, so it was
resized until the occupied port sits inside a third of the candidate blocks.

Also fixes an off-by-one inherited from #1014: the width guard tested hi - n <= lo,
which rejected a window sized to hold the block exactly even though that window has a
valid anchor, and reported "too narrow" for a window that was not.
…tes (#1103)

The SQLite smoke and the Postgres pool leg both drew their API and sink bases from a
single ephemeral probe and let the runner increment off them. Both now reserve the
whole range.

The Postgres leg carried a comment defending the old shape -- draw the sink first so
the API block increments away from it. That only ever separated these two families
from each other, and only because back-to-back ephemeral draws happen to be adjacent;
it said nothing about the rest of the machine, which is where both observed CI reds
came from. Disjoint fixed windows make the separation structural instead.

The allocator and its guards moved to tests/_connscale_ports.py and
tests/test_connscale_ports.py, which also keeps this file's port region small: #1210
touches the FD/RSS region of the smoke test and the two no longer overlap.
… last two violations

The coordinator's integration commit for wave 2, and the single point where the
"a PR that implements BACKLOG #N must update BACKLOG.md" required context is
satisfied for the whole train. Five lanes touch messagefoundry/ or scripts/ and
none may edit the ledger, so as independent pull requests they could not satisfy
it. Banner text is each lane's own, carried verbatim.

CLOSED (15): 333 344 1012 1033 1035 1036 1052 1053 1076 1079 1089 1090 1092
             1095 1103

STILL OPEN, deliberately, each with one banner and a named residue:
  1040 -- the audit is complete and two surfaces are treated, but claim_check.py
          still interpolates a claim's session-supplied `note` into a stderr block
          ending in a runnable release command, and whether the two helper copies
          become a shared module is an owner call (the gate is installed OUTSIDE
          every working tree and can dot-source nothing from a checkout).
  1210 -- arms 1 and 4 shipped, arm 2 (record the covering PID set) is unbuilt and
          arm 3 is rejected on measurement, so the FD/RSS sum still cannot tell a
          legitimate growth from a misresolution.

Also closes the last two path-bearing citation violations, which lived in
BACKLOG-CLOSED.md and were outside the building lane's permitted file set:
#325 and #322 each cited ../../BACKLOG.md while both items live in the archive.
Confirmed by heading count (1 in the archive, 0 in the live file) rather than by
the line numbers, which had already drifted. The new gate now passes repo-wide,
not merely diff-scoped: 272 citations in scope across 347 markdown files, exit 0
with 4 advisory warnings for items above the published baseline.

Ledger, re-derived with parse_items rather than carried forward: live 241,
open 209, closed-in-live 32, archive 236, namespace 477 conserved. 224 open at
wave-2 start, 209 now.
Caught at integration, not in the lane, and the lane could not have caught it.

`_run` passed `encoding="utf-8"`, which governs how the PARENT decodes and says
nothing about how the child ENCODES. The checker prints an em dash; a child
inheriting a stock Windows console writes it as cp1252 0x97, which is not valid
UTF-8, so the reader thread dies with UnicodeDecodeError and `stdout` arrives as
None. The four diff-scope assertions then fail with

    TypeError: argument of type 'NoneType' is not a container or iterable

which names neither the cause nor the failing property.

WHY THE LANE SAW GREEN: its shell exported PYTHONIOENCODING=utf-8 -- the wave
brief tells workers to set it for backlog_status_check -- so the child inherited
utf-8 and the file passed. The green depended on the ambient environment rather
than on the code, and it would have red-ed CI's Windows legs while staying green
on ubuntu, where utf-8 is the default.

MEASURED on this branch, three ambient conditions:
  PYTHONIOENCODING unset   before 4 failed / 19 passed   after 23 passed
  PYTHONIOENCODING=utf-8   before 23 passed              after 23 passed
  PYTHONIOENCODING=cp1252  (hostile)                     after 23 passed
The third is the one that matters: it proves the child env OVERRIDES rather than
inheriting a lucky value, so the fix is not itself environment-dependent.

This is the #1030 class -- a non-cp1252 character in output that only some
environments can carry -- which is still open and is the general form of it.
@wshallwshall
wshallwshall merged commit d5ff180 into main Aug 10, 2026
51 of 52 checks passed
@wshallwshall
wshallwshall deleted the w2-integration branch August 10, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant